Skip to content

perf: durable Columnar DELETE in-place via tombstone markers (no flush rewrite) - #367

Merged
MPCoreDeveloper merged 1 commit into
masterfrom
perf/tombstone-delete
Sep 3, 2026
Merged

perf: durable Columnar DELETE in-place via tombstone markers (no flush rewrite)#367
MPCoreDeveloper merged 1 commit into
masterfrom
perf/tombstone-delete

Conversation

@MPCoreDeveloper

Copy link
Copy Markdown
Owner

Why

Master #366 made Columnar DELETE durable by compacting at flush/dispose, but the rewrite cost dropped --pk DELETE to ~18.6K ops/s (0.54s per 10K deletes on a ~100K-row table).

This PR makes the durable delete in-place: a tombstone marker is written over the record's 4-byte length prefix (negative slot size), and every raw record enumerator/compactor skips the slot. DELETE survives a reopen in O(delete) — no flush-time rewrite.

Root cause this PR fixes

The earlier tombstone attempt was reverted because the ULID-migration flow (delete + insertCompactStorage) produced an empty .dat (rows=1, filelen=0). Cause found: AppendOnlyEngine.CompactTable did break on any non-positive prefix, so the first tombstone stopped the walk and nothing active was rewritten. It now skips negative prefixes like every other enumerator.

Rollback safety

A tombstone written before a transaction commits would survive a rollback that restores the row in the PK index. Transactional deletes are therefore not tombstoned at delete time; they are physically removed by the post-commit flush/dispose compaction against the current PK — the rollback-safe semantics of #366. Non-transactional DELETE (the measured hot path) never pays that rewrite.

Measured (--pk harness, DELETE 10K of ~100K rows)

master #366 (flush rewrite) this PR (tombstones)
legacy variable-length ~0.54s — ~18.6K ops/s ~0.16s — ~63.7K ops/s
fixed-width ~0.54s — ~18.6K ops/s ~0.12s — ~81.0K ops/s (gap vs SQLite ~4.2×)

DELETE is again on par with UPDATE (FW UPDATE ~86.7K ops/s). Tombstone space is reclaimed by the tombstone-aware CompactTable/CompactStorage (explicit, auto-compact threshold, ULID migration).

Validation

  • Full SharpCoreDB.Tests suite (Debug + Release/CI filter): green (1656 tests, incl. LegacyUlidMigrationTests, ULID, bulk-delete, reopen, fixed-width, transaction suites).
  • CI-equivalent local run (Release, Category!=Debug&Category!=Manual&Category!=Performance): SharpCoreDB.Tests, SharpCoreDB.VectorSearch.Tests, SharpCoreDB.EntityFrameworkCore.Tests, SharpCoreDB.Functional.Linq2DB.Tests — all EXIT=0.
  • CHANGELOG updated.

…h rewrite)

Non-transactional Columnar deletes now write a tombstone marker over the record's 4-byte length prefix (negative slot size) instead of queueing a flush-time full-file rewrite, so DELETE survives a reopen in O(delete). --pk harness DELETE: ~18.6K ops/s -> ~64K (legacy) / ~81K ops/s (fixed-width), back on par with UPDATE.

- IStorage.TombstoneRecord (default false) + Storage implementation (read slot size, write -slotSize marker, evict page cache).

- Every raw record enumerator/compactor skips negative prefixes: Storage.ReadAllRecords, AppendOnlyEngine.GetAllRecords + CompactTable, Table.Scanning/CRUD/ParallelScan.

- CompactTable broke on a negative prefix, which made the ULID-migration compaction (delete+insert then CompactStorage) write an empty file; it now skips tombstones so migration stays correct.

- Transactional deletes are NOT tombstoned at delete time (a marker would survive a rollback that restores the row in the PK index); they are physically removed by the post-commit flush/dispose compaction against the current PK, keeping the rollback-safe semantics of #366.

- Tombstone space is reclaimed by the tombstone-aware CompactStorage/CompactTable (explicit, auto-compact threshold, ULID migration). CHANGELOG updated.
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant